﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Microsoft.Practices.EnterpriseLibrary.Caching;
using Microsoft.Practices.EnterpriseLibrary.Caching.Expirations;
using MRS.FrontEnd.BO;
//using Microsoft.Reputation.Services.PivotDb;

namespace Microsoft.Reputation.Services
{
    internal class OverrideThreadObject
    {
        private long m_lastOverrideStamp;
        private IDataProvider m_dataProvider;
        private ICacheManager m_cacheManager;
        private int m_queryInterval;

        private static OverrideThreadObject OverrideThread;

        // Hold the wait handle telling the service to terminate
        private ManualResetEvent ShutdownEvent = new ManualResetEvent(false);

        public static void Initialize()
        {
            OverrideThreadObject.OverrideThread = new OverrideThreadObject();

            Thread overrideThread = new Thread(new ThreadStart(OverrideThread.Run));
            overrideThread.Start();
            TraceUtility.TraceInfo(Tracing.Source, " Override Thread Run called {0}", DateTime.Now);
        }

        public static void Shutdown()
        {
            OverrideThreadObject.OverrideThread.ShutdownEvent.Set();
        }

        /// <summary>
        /// Standard constructor that will be called during the IIS service opening
        /// </summary>
        public OverrideThreadObject()
        {
            this.InitializeThis();

            m_dataProvider = MrsDeliveryService.DataProvider;
            m_cacheManager = CacheFactory.GetCacheManager();
            m_queryInterval = DeliveryServiceConfig.OverrideQueryIntervalInSeconds;
        }

        /// <summary>
        /// This constructor is meant for dependency injection
        /// </summary>
        /// <param name="dataProvider"></param>
        /// <param name="cacheManager"></param>
        /// <param name="queryInterval"></param>
        internal OverrideThreadObject(IDataProvider dataProvider, ICacheManager cacheManager, int queryInterval)
        {
            this.InitializeThis();
            m_dataProvider = dataProvider;
            m_cacheManager = cacheManager;
            m_queryInterval = queryInterval;
            OverrideThreadObject.OverrideThread = this;
        }

        /// <summary>
        /// This is used by test code to inject values.  It should NOT
        /// be used in normal operation of code
        /// </summary>
        internal long LastOverrideStamp
        {
            set { m_lastOverrideStamp = value; }
        }

        private void InitializeThis()
        {
            m_lastOverrideStamp = 0;
        }

        /// <summary>
        /// Thread Run method
        /// During the processing of this method we will do the following steps
        /// 1) Query the database for the current overrides
        /// 2) Update the Delivery Service cache with the overrides
        /// 3) Update the override list in the Delivery Service so clients will get them
        /// 4) Sleep then start over
        /// </summary>
        public void Run()
        {
            TraceUtility.TraceInfo(Tracing.Source, " Override Thread started at {0}", DateTime.Now);

            TimeSpan queryTimeSpan = new TimeSpan(0, 0, m_queryInterval);

            do
            {
                try
                {
                    // Query the Database for updates
                    long latestOverrideStamp = m_lastOverrideStamp;
                    
                    List<Override> overridesFromDB = this.QueryDBForOverrides(ref latestOverrideStamp);

                    if (latestOverrideStamp < m_lastOverrideStamp)
                    {
                        TraceUtility.TraceWarning(
                            Tracing.Source,
                            "The last override stamp from the database {0}, was less than the last stamp we got {1}",
                            overridesFromDB.Last().Id,
                            m_lastOverrideStamp);
                        continue;
                    }

                    if (latestOverrideStamp > m_lastOverrideStamp)
                    {
                        m_lastOverrideStamp = latestOverrideStamp;

                        this.UpdateCache(overridesFromDB);
                        this.UpdateOverrideListInDeliveryService(overridesFromDB);
                    }
                    EventLogger.LogGetOverridesThreadSuccess();
                }
                catch (Exception ex)
                {
                    EventLogger.LogGetOverridesThreadFailure(ex.Message);
                    TraceUtility.TraceCatchException(Tracing.Source, ex);
                }
            } while (this.ShutdownEvent.WaitOne(queryTimeSpan) == false);

            TraceUtility.TraceInfo(Tracing.Source, "Override Thread shutting down at {0}", DateTime.Now);
        }

        /// <summary>
        /// Query the database by asking the DataProvider for the current overrides
        /// </summary>
        /// <param name="latestOverrideStamp">The last override stamp that we got from the database.  
        /// It will be returned as the last override stamp in the list returned.</param>
        /// <returns>A list of override ids and objects to be overriden.</returns>
        internal List<Override> QueryDBForOverrides(ref long latestOverrideStamp)
        {
            DPOverrideResponse dpOverrideResponse = m_dataProvider.GetOverrides(latestOverrideStamp);

            List<Override> overrides;
            if (dpOverrideResponse.OperationStatus == StatusCodes.OK)
            {
                overrides = dpOverrideResponse.Overrides;
                if (overrides.Count != 0)
                {
                    latestOverrideStamp = overrides.Max(o => o.Id);
                }

                TraceUtility.TraceInfo(
                    Tracing.Source,
                    " [INFO] GetOverrides {1} overrides returned",
                    overrides.Count.ToString());
            }
            else
            {
                TraceUtility.TraceError(Tracing.Source, "GetOverrides returned a status code of {0:x}", dpOverrideResponse.OperationStatus);
                overrides = new List<Override>();
            }

            return overrides;
        }

        /// <summary>
        /// Push the overrides into the cache
        /// This is done differently than a normal add to the cache as we want to make sure 
        /// that the entry persists in the cache until the expiration time of the override.
        /// </summary>
        /// <param name="repuObjectsToUpdate">The list of objects to update in the cache</param>
        internal void UpdateCache(List<Override> objectsToUpdate)
        {
            // We check to see if the cache is enabled because if it is not we do not want
            // to set things into the PNP Cache as they will never be retrieved
            if (DeliveryServiceConfig.ReputationCacheEnabled == false)
            {
                return;
            }

            DateTime now = DateTime.UtcNow;

            foreach (Override overrideObj in objectsToUpdate)
            {
                // Currently this is hardcoded to 4 which is the URL Type
                byte[] sha256 = overrideObj.unpackedObject.Sha256;

                if (sha256 != null)
                {
                    TimeSpan experationSpan = overrideObj.Expiration.Value.Subtract(now);

                    if (experationSpan.TotalSeconds > 0)
                    {
                        byte[] packedObjectFromCache = null;
                        string cacheKey = PNPCacheProvider.GetCacheKey(overrideObj.unpackedObject.Type, sha256);

                        // Check the versions of the objects ot make sure that we are not trying
                        // to insert an older object in to the cache
                        packedObjectFromCache = m_cacheManager.GetData(cacheKey) as byte[];
                        if (packedObjectFromCache != null)
                        {
                            IReputationBinaryCodec codec = UnpackedObject.ReputationBinaryCodecFactory(overrideObj.unpackedObject.Type);
                            UnpackedObject unpackedFromCache = codec.UnpackObjectBinary(packedObjectFromCache);

                            if (unpackedFromCache.Version >= overrideObj.unpackedObject.Version)
                            {
                                // The object we got from the database is older so don't bother to pack it or
                                // put it in the cache
                                continue;
                            }
                        }

                        m_cacheManager.Add(cacheKey, overrideObj.packedObject, CacheItemPriority.NotRemovable, null,
                            new AbsoluteTime(experationSpan));
                    }
                }
                else
                {
                    TraceUtility.TraceError(Tracing.Source, "SHA256 was not returned for the ReputationObject {0}", overrideObj.unpackedObject.Sha256);
                }
            }
        }

        /// <summary>
        /// Updates the override list and the last override stamp in the Delivery Service so that clients
        /// can begin to receive the overrides
        /// </summary>
        /// <param name="overridesFromDB">Overrides list that came from the database</param>
        internal void UpdateOverrideListInDeliveryService(List<Override> overridesFromDB)
        {
            if (overridesFromDB.Count > 0)
            {
                SortedList<long, byte[]> overrideList = new SortedList<long, byte[]>(MrsDeliveryService.OverrideHashesFromBackEnd);

                DateTime now = DateTime.UtcNow;

                foreach (Override overrideObj in overridesFromDB)
                {
                    if ((overrideList.ContainsKey(overrideObj.Id) == false)
                        && (overrideObj.Expiration.HasValue)
                        && (overrideObj.Expiration.Value.CompareTo(now) > 0))
                    {
                        overrideList.Add(overrideObj.Id, overrideObj.unpackedObject.Sha256);
                    }
                }

                MrsDeliveryService.OverrideHashesFromBackEnd = overrideList;
            }

            MrsDeliveryService.LastKnownOverrideStamp = m_lastOverrideStamp;
        }
    }
}
